#!/usr/bin/env bash

include "test.sh"
include "regex.sh"

declare arguments_include_cnt=0
function arguments()
{
    ((arguments_include_cnt++))
    eval "arguments_${arguments_include_cnt}_index=-1"
    eval "arguments_${arguments_include_cnt}_array=($*)"
    eval "arguments_${arguments_include_cnt}_length=$#"
    eval "arguments_${arguments_include_cnt}_skipped=()"
    eval "arguments_${arguments_include_cnt}_now=''"

    function arguments::is_end()
    {
        local index_name="arguments_${arguments_include_cnt}_index"
        local length_name="arguments_${arguments_include_cnt}_length"
        (( ${!index_name} == ${!length_name} ))
    }

    function arguments::next()
    {
        if arguments::is_end; then
            assert_fail "no more argument" || return
        fi

        eval "((arguments_${arguments_include_cnt}_index++))"
        local index_name="arguments_${arguments_include_cnt}_index"
        local index="${!index_name}"
        local array="arguments_${arguments_include_cnt}_array"
        eval "arguments_${arguments_include_cnt}_now=\"\${$array[$index]}\""
    }

    function arguments::skip()
    {
        eval "arguments_${arguments_include_cnt}_skipped+=('$(arguments::get)')"
        arguments::next
    }

    function arguments::skip_all()
    {
        while ! arguments::is_end; do
            arguments::skip
        done
    }

    function arguments::is_kv_option()
    {
        [[ "$(arguments::get)" =~ (^--${regex_id2}=) ]]
    }

    function arguments::is_option()
    {
        local now="$(arguments::get)"
        [ "${now:0:1}" == "-" ]
    }

    function arguments::get()
    {
        eval "echo \${arguments_${arguments_include_cnt}_now}"
    }

    function arguments::get_option()
    {
        local option="$(arguments::get)"
        while [ "${option:0:1}" == "-" ]; do
            option="${option:1}"
        done
        echo $option
    }

    function arguments::get_key()
    {
        if ! arguments::is_kv_option; then
            assert_fail "get_key need is_kv_option is true" || return
        fi

        local option="$(arguments::get)"
        option="${option:2}"
        echo ${option%%=*}
    }

    function arguments::get_val()
    {
        if ! arguments::is_kv_option; then
            assert_fail "get_val need is_kv_option is true" || return
        fi

        local option="$(arguments::get)"
        echo ${option#*=}
    }

    function arguments::get_skipped()
    {
        eval "echo \${arguments_${arguments_include_cnt}_skipped[*]}"
    }

    function arguments::get_rest()
    {
        local index_name="arguments_${arguments_include_cnt}_index"
        local length_name="arguments_${arguments_include_cnt}_length"
        local array_name="arguments_${arguments_include_cnt}_array"
        local index="${!index_name}"
        local length="${!length_name}"
        local i
        for (( i = index; i < length; i++ )); do
            eval "echo -n \"\${$array_name[$i]} \""
        done
    }

    function arguments::process_option()
    {
        # $1: 遇到无参时操作模式 {null/help}
        # $2: 遇到非选项时操作模式 {skip/break}
        # $3: 选项处理器函数前缀

        local index_name="arguments_${arguments_include_cnt}_index"
        assert ${!index_name} == 0 "调用process_option之前已被解析过了"

        if arguments::is_end; then
            case "$1" in
                null) ;;
                help) arguments::destroy && arguments "--help" ;;
                *) assert_fail "不认识的无参操作模式: $1" || return ;;
            esac
        fi

        while ! arguments::is_end; do
            if ! arguments::is_option; then
                case "$2" in
                    skip) arguments::skip && continue ;;
                    break) arguments::skip_all && break ;;
                    *) assert_fail "不认识的非选项操作模式: $2" || return ;;
                esac
            fi

            local option="$(arguments::get)"
            arguments::next

            local func="${3}${option}"
            if ! declare -f $func 1>/dev/null; then
                loge "选项: ${option}不存在"
                return 1
            else
                $func
            fi
        done
    }

    function arguments::use_skip()
    {
        local skipped=($(arguments::get_skipped))
        arguments::destroy
        arguments ${skipped[*]}
    }

    function arguments::destroy()
    {
        # delete variable
        unset "arguments_${arguments_include_cnt}_index"
        unset "arguments_${arguments_include_cnt}_array"
        unset "arguments_${arguments_include_cnt}_length"
        unset "arguments_${arguments_include_cnt}_skipped"
        unset "arguments_${arguments_include_cnt}_now"

        ((arguments_include_cnt--))

        # delete function
        if (( arguments_include_cnt == 0 )); then
            unset -f arguments::is_end
            unset -f arguments::next
            unset -f arguments::skip
            unset -f arguments::is_kv_option
            unset -f arguments::is_option
            unset -f arguments::get
            unset -f arguments::get_option
            unset -f arguments::get_key
            unset -f arguments::get_val
            unset -f arguments::get_skipped
            unset -f arguments::get_rest
            unset -f arguments::process_option
            unset -f arguments::use_skip
            unset -f arguments::destroy
        fi
    }

    arguments::next
}

function arguments::test1()
{
    arguments -c --output a --index 2 --success --filter="123" normal

    # -c
    test::should_return_false arguments::is_end
    test::should_return_true arguments::is_option
    test::should_return_false arguments::is_kv_option
    test::run_cmd arguments::get
    test::should_be -c
    test::run_cmd arguments::get_option
    test::should_be c
    arguments::next

    # --output a
    test::should_return_false arguments::is_end
    test::should_return_true arguments::is_option
    test::should_return_false arguments::is_kv_option
    test::run_cmd arguments::get
    test::should_be --output
    test::run_cmd arguments::get_option
    test::should_be output
    arguments::next

    test::should_return_false arguments::is_end
    test::should_return_false arguments::is_option
    test::run_cmd arguments::get
    test::should_be a
    test::run_cmd arguments::get_option
    test::should_be a
    arguments::next

    # --index 2
    test::should_return_false arguments::is_end
    test::should_return_true arguments::is_option
    test::should_return_false arguments::is_kv_option
    arguments::skip

    test::should_return_false arguments::is_end
    test::should_return_false arguments::is_option
    arguments::skip

    test::run_cmd arguments::get_skipped
    test::should_be "--index 2"
    skipped=($(arguments::get_skipped))
    test::run_cmd echo ${skipped[0]}
    test::should_be --index

    # --success
    test::should_return_false arguments::is_end
    test::should_return_true arguments::is_option
    test::should_return_false arguments::is_kv_option
    test::run_cmd arguments::get
    test::should_be --success
    test::run_cmd arguments::get_option
    test::should_be success
    arguments::next

    # --filter="123"
    test::should_return_false arguments::is_end
    test::should_return_true arguments::is_option
    test::should_return_true arguments::is_kv_option
    test::run_cmd arguments::get
    test::should_be --filter="123"
    test::run_cmd arguments::get_key
    test::should_be filter
    test::run_cmd arguments::get_val
    test::should_be 123
    arguments::next

    # normal
    test::should_return_false arguments::is_end
    test::should_return_false arguments::is_option
    test::run_cmd arguments::get
    test::should_be normal
    arguments::next

    test::should_return_true arguments::is_end
    arguments::destroy
}
test::register_test arguments::test1

function arguments::test2()
{
    arguments -c
        arguments -a

        # -a
        test::should_return_false arguments::is_end
        test::should_return_true arguments::is_option
        test::run_cmd arguments::get
        test::should_be -a
        test::run_cmd arguments::get_option
        test::should_be a
        arguments::next

        test::should_return_true arguments::is_end
        arguments::destroy

    # -c
    test::should_return_false arguments::is_end
    test::should_return_true arguments::is_option
    test::run_cmd arguments::get
    test::should_be -c
    test::run_cmd arguments::get_option
    test::should_be c
    arguments::next

    test::should_return_true arguments::is_end
    arguments::destroy
}
test::register_test arguments::test2

function arguments::test3()
{
    arguments - --

    # -
    test::should_return_false arguments::is_end
    test::should_return_true arguments::is_option
    test::run_cmd arguments::get
    test::should_be -
    test::run_cmd arguments::get_option
    test::should_be ""
    arguments::next

    # -
    test::should_return_false arguments::is_end
    test::should_return_true arguments::is_option
    test::run_cmd arguments::get
    test::should_be --
    test::run_cmd arguments::get_option
    test::should_be ""
    arguments::next

    test::should_return_true arguments::is_end
    arguments::destroy
}
test::register_test arguments::test3

function arguments::test4()
{
    arguments 1 2 3

    # 1
    test::should_return_false arguments::is_end
    test::should_return_false arguments::is_option
    test::run_cmd arguments::get
    test::should_be "1"
    arguments::next

    test::run_cmd arguments::get_rest
    test::should_be '2 3 '
}
test::register_test arguments::test4

function arguments::test5()
{
    arguments 1 2 3
    arguments::next # 1
    arguments::skip # 2
    arguments::skip # 3
    arguments::use_skip

    # 2
    test::run_cmd arguments::get
    test::should_be "2"
    arguments::next

    # 3
    test::run_cmd arguments::get
    test::should_be "3"
    arguments::next
}
test::register_test arguments::test5
